在 Service Worker 的 fetch 事件中,我们往往会从本地缓存中构建请求结果从而加速响应,然而有些时候我们又需要通过网络请求获取最新的数据,那么如何决定缓存的使用时机呢?本章将介绍一些常见的请求策略,以便大家能够更容易地控制缓存的使用时机。

# 常见策略

# 缓存优先

首先从缓存中进行匹配,如果存在相关请求的响应,返回该响应,否则通过网络获取。基本实现如下:

async function fetchFromNetwork(event) {
  const response = await fetch(event.request);
  if (response) {
    const cloneResponse = response.clone();
    event.waitUntil((async () => {
      const cache = await caches.open('cache-name');
      await cache.put(event.request, cloneResponse);
    })());
  }
  return response;
}

self.addEventListener('fetch', event => {
  if (request.method.toLowerCase() === 'get') {
    event.respondWith((async () => {
      const cachedResponse = await caches.match(event.request);
      if (cachedResponse) {
        return cachedResponse;
      }
      return await fetchFromNetwork(event);
    })());
  }
});

该策略主要适用于请求资源不经常变更的情况,比如:Shell 文件、图片、脚本等。

# 网络优先

首先通过网络获取,如果请求异常,则从缓存中获取。基本实现如下:

self.addEventListener('fetch', event => {
  if (request.method.toLowerCase() === 'get') {
    event.respondWith((async () => {
      try {
        return await fetchFromNetwork(event);
      } catch {
        return await caches.match(event.request);
      }
    })());
  }
});

该策略主要用于需要频繁更新的资源,比如:资讯、排行榜等。

该策略的主要优势是,如果用户处于离线状态,依旧可以为其提供服务,从而为用户提供更好的使用体验。

# 仅使用缓存

所有请求都从缓存中获取,基本实现如下:

self.addEventListener('fetch', event => {
  if (request.method.toLowerCase() === 'get') {
    event.respondWith(caches.match(event.request));
  }
});

该策略的使用场景与缓存优先类似,相对于后者,该策略的主要问题是,如果缓存中不存在相关请求的响应,它将与传统的网络请求一样抛出异常,这可能会导致令人失望的用户体验。

# 仅使用网络

  • 所有请求都从网络中获取,这是浏览器的默认行为,无需在 Service Worker 中做任何特殊处理。
  • 该策略的使用场景与网络优先类似,相对于后者,该策略的主要问题是,如果请求出现异常,这可能会导致令人失望的用户体验。

# 先缓存后网络

该策略为缓存优先的升级版,它与后者的唯一区别是,如果在缓存中匹配到相关请求的响应,在返回该响应的同时依旧会发起网络请求,并更新相关缓存。基本实现如下:

self.addEventListener('fetch', event => {
  if (request.method.toLowerCase() === 'get') {
    event.respondWith((async () => {
      const cachedResponse = await caches.match(event.request);
      if (cachedResponse) {
        try {
          event.waitUntil((async () => {
            await fetchFromNetwork(event);
          })());
        } catch {
        }
        return cachedResponse;
      }
      return await fetchFromNetwork(event);
    })());
  }
});

该策略适用于任何类型的资源,其最为常见的一个场景是,假设一个处于滚动的列表,为了不让用户感觉到因请求最新数据而导致的间断,我们可以使用该策略快速返回缓存版本的数据,当滚动停止时,便可以用得到的最新数据替换展示在用户面前的内容。

# 总结

上文中,我们讨论了常见的请求策略,它为我们如何决定使用缓存提供了理论基础。很多情况下,我们不必为某一个请求选择一个具体的策略,而是根据其特点综合使用多种策略,比如示例

async function fetchPageContent(cacheKey, event) {
  try {
    const response = await fetch(cacheKey, {
      headers: {
        'only_content': 1
      }
    });
    if (response) {
      const cloneResponse = response.clone();
      event.waitUntil((async () => {
        await setCache(runtimeCacheName, cacheKey, cloneResponse);
      })());
    }
    return response;
  } catch {
    return await getCache(runtimeCacheName, cacheKey);
  }
}

function fetchPage(cacheKey, event) {
  //... 根据 cacheKey 获取 shell 类型
  const stream = new ReadableStream({
    start(controller) {
      //... pushStream 函数定义
      (async () => {
        const top = await getCache(precacheName, `/shell/${shellType}_top.html`);
        await pushStream(top.body);
        const content = await fetchPageContent(cacheKey, event);
        await pushStream(content.body);
        const bottom = await getCache(precacheName, `/shell/${shellType}_bottom.html`);
        await pushStream(bottom.body);
        controller.close();
      })();
    }
  });

  return new Response(stream, {
    headers: { 'Content-Type': 'text/html' }
  });
}

fetchPage 中,shell 文件(topbottom)的获取使用了仅使用缓存策略,正文信息(content)的获取使用了网络优先策略(通过调用 fetchPageContent

阅读全文